/* eslint-disable camelcase */
import { $set } from '@feng3d/serialization';
import { Color, MathUtils3D, MeshPhongMaterial3D, Node3D, PerspectiveCamera3D, Scene3D, SphereGeometry3D, StatsComponent, WebGLRenderer3D } from 'feng3d-three';
import { TextGeometry3D } from '../../extension/geometries/TextGeometry3D';
import { FontLoader3D } from '../../extension/loaders/FontLoader3D';
import { Font } from '../../extension/thirdparty/three/imports';

// 1 micrometer to 100 billion light years in one scene, with 1 unit = 1 meter?  preposterous!  and yet...
const NEAR = 1e-6;
const FAR = 1e27;
let SCREEN_WIDTH = window.innerWidth;
let SCREEN_HEIGHT = window.innerHeight;
let screensplit = 0.25;
let screensplit_right = 0;
const mouse = [0.5, 0.5];
let zoompos = -100;
let minzoomspeed = 0.015;
let zoomspeed = minzoomspeed;

let container: HTMLElement;
const root = new Node3D();
let border: HTMLElement;
let stats: StatsComponent;
const objects: {
    [name: string]: {
        container: HTMLElement;
        renderer: WebGLRenderer3D;
        scene: Scene3D;
        camera: PerspectiveCamera3D;
    }
} = {};

// Generate a number of text labels, from 1µm in size up to 100,000,000 light years
// Try to use some descriptive real-world examples of objects at each scale

const labeldata = [
    { size: 0.01, scale: 0.0001, label: 'microscopic (1µm)' }, // FIXME - triangulating text fails at this size, so we scale instead
    { size: 0.01, scale: 0.1, label: 'minuscule (1mm)' },
    { size: 0.01, scale: 1.0, label: 'tiny (1cm)' },
    { size: 1, scale: 1.0, label: 'child-sized (1m)' },
    { size: 10, scale: 1.0, label: 'tree-sized (10m)' },
    { size: 100, scale: 1.0, label: 'building-sized (100m)' },
    { size: 1000, scale: 1.0, label: 'medium (1km)' },
    { size: 10000, scale: 1.0, label: 'city-sized (10km)' },
    { size: 3400000, scale: 1.0, label: 'moon-sized (3,400 Km)' },
    { size: 12000000, scale: 1.0, label: 'planet-sized (12,000 km)' },
    { size: 1400000000, scale: 1.0, label: 'sun-sized (1,400,000 km)' },
    { size: 7.47e12, scale: 1.0, label: 'solar system-sized (50Au)' },
    { size: 9.4605284e15, scale: 1.0, label: 'gargantuan (1 light year)' },
    { size: 3.08567758e16, scale: 1.0, label: 'ludicrous (1 parsec)' },
    { size: 1e19, scale: 1.0, label: 'mind boggling (1000 light years)' }
];

init();

function init()
{
    container = document.getElementById('container');

    const loader = new FontLoader3D();
    loader.load('../../fonts/helvetiker_regular.typeface.json', function (font)
    {
        const scene = initScene(font);

        // Initialize two copies of the same scene, one with normal z-buffer and one with logarithmic z-buffer
        objects.normal = initView(scene, 'normal', false);
        objects.logzbuf = initView(scene, 'logzbuf', true);

        animate();
    });

    stats = root.addComponent('StatsComponent');
    container.appendChild(stats.dom);

    // Resize border allows the user to easily compare effects of logarithmic depth buffer over the whole scene
    border = document.getElementById('renderer_border');
    border.addEventListener('pointerdown', onBorderPointerDown);

    window.addEventListener('mousemove', onMouseMove);
    window.addEventListener('resize', onWindowResize);
    window.addEventListener('wheel', onMouseWheel);
}

function initView(scene: Scene3D, name: string, logDepthBuf: boolean)
{
    const framecontainer = document.getElementById(`container_${name}`);

    const camera = new Node3D().addComponent('PerspectiveCamera3D', {
        fov: 50, aspect: screensplit * SCREEN_WIDTH / SCREEN_HEIGHT, near: NEAR, far: FAR
    });
    scene.entity.addChild(camera.entity);

    const renderer = new Node3D().addComponent('WebGLRenderer3D', {
        parameters: { antialias: true, logarithmicDepthBuffer: logDepthBuf },
    });
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(SCREEN_WIDTH / 2, SCREEN_HEIGHT);
    renderer.domElement.style.position = 'relative';
    renderer.domElement.id = `renderer_${name}`;
    framecontainer.appendChild(renderer.domElement);

    return { container: framecontainer, renderer, scene, camera };
}

function initScene(font: Font)
{
    const scene = root.addComponent('Scene3D');

    scene.addComponent('AmbientLight3D', { color: { value: 0x222222 } });

    const light = new Node3D().addComponent('DirectionalLight3D', { color: { value: 0xffffff }, intensity: 1 });
    light.entity.position.set(100, 100, 100);
    scene.entity.addChild(light.entity);

    const materialargs = {
        color: new Color(0xffffff),
        specular: new Color(0x050505),
        shininess: 50,
        emissive: new Color(0x000000),
    };

    const geometry = $set(new SphereGeometry3D(), { radius: 0.5, widthSegments: 24, heightSegments: 12 });

    for (let i = 0; i < labeldata.length; i++)
    {
        const scale = labeldata[i].scale || 1;

        const labelgeo = $set(new TextGeometry3D(), {
            text: labeldata[i].label, parameters: {
                font,
                size: labeldata[i].size,
                height: labeldata[i].size / 2
            }
        });

        labelgeo.computeBoundingSphere();

        // center text
        labelgeo.translate(-labelgeo.boundingSphere.radius, 0, 0);

        materialargs.color = new Color().setHSL(Math.random(), 0.5, 0.5);

        const material = $set(new MeshPhongMaterial3D(), materialargs);

        const group = new Node3D();
        group.position.z = -labeldata[i].size * scale;
        scene.entity.addChild(group);

        const textmesh = new Node3D().addComponent('Mesh3D', {
            geometry: labelgeo,
            material,
        });
        textmesh.entity.scale.set(scale, scale, scale);
        textmesh.entity.position.z = -labeldata[i].size * scale;
        textmesh.entity.position.y = labeldata[i].size / 4 * scale;
        group.addChild(textmesh.entity);

        const dotmesh = new Node3D().addComponent('Mesh3D', {
            geometry, material
        });
        dotmesh.entity.position.y = -labeldata[i].size / 4 * scale;
        dotmesh.entity.scale.multiplyScalar(labeldata[i].size * scale);
        group.addChild(dotmesh.entity);
    }

    return scene;
}

function updateRendererSizes()
{
    // Recalculate size for both renderers when screen size or split location changes

    SCREEN_WIDTH = window.innerWidth;
    SCREEN_HEIGHT = window.innerHeight;

    screensplit_right = 1 - screensplit;

    objects.normal.renderer.setSize(screensplit * SCREEN_WIDTH, SCREEN_HEIGHT);
    objects.normal.camera.aspect = screensplit * SCREEN_WIDTH / SCREEN_HEIGHT;
    objects.normal.camera.updateProjectionMatrix();
    objects.normal.camera.setViewOffset(SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0, SCREEN_WIDTH * screensplit, SCREEN_HEIGHT);
    objects.normal.container.style.width = `${screensplit * 100}%`;

    objects.logzbuf.renderer.setSize(screensplit_right * SCREEN_WIDTH, SCREEN_HEIGHT);
    objects.logzbuf.camera.aspect = screensplit_right * SCREEN_WIDTH / SCREEN_HEIGHT;
    objects.logzbuf.camera.updateProjectionMatrix();
    objects.logzbuf.camera.setViewOffset(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_WIDTH * screensplit, 0, SCREEN_WIDTH * screensplit_right, SCREEN_HEIGHT);
    objects.logzbuf.container.style.width = `${screensplit_right * 100}%`;

    border.style.left = `${screensplit * 100}%`;
}

function animate()
{
    requestAnimationFrame(animate);
    render();
}

function render()
{
    // Put some limits on zooming
    const minzoom = labeldata[0].size * labeldata[0].scale * 1;
    const maxzoom = labeldata[labeldata.length - 1].size * labeldata[labeldata.length - 1].scale * 100;
    let damping = (Math.abs(zoomspeed) > minzoomspeed ? 0.95 : 1.0);

    // Zoom out faster the further out you go
    const zoom = MathUtils3D.clamp(Math.pow(Math.E, zoompos), minzoom, maxzoom);
    zoompos = Math.log(zoom);

    // Slow down quickly at the zoom limits
    if ((zoom === minzoom && zoomspeed < 0) || (zoom === maxzoom && zoomspeed > 0))
    {
        damping = 0.85;
    }

    zoompos += zoomspeed;
    zoomspeed *= damping;

    objects.normal.camera.entity.position.x = Math.sin(0.5 * Math.PI * (mouse[0] - 0.5)) * zoom;
    objects.normal.camera.entity.position.y = Math.sin(0.25 * Math.PI * (mouse[1] - 0.5)) * zoom;
    objects.normal.camera.entity.position.z = Math.cos(0.5 * Math.PI * (mouse[0] - 0.5)) * zoom;
    objects.normal.camera.entity.lookAt(objects.normal.scene.entity.position);

    // Clone camera settings across both scenes
    objects.logzbuf.camera.entity.position.copy(objects.normal.camera.entity.position);
    objects.logzbuf.camera.entity.quaternion.copy(objects.normal.camera.entity.quaternion);

    // Update renderer sizes if the split has changed
    if (screensplit_right !== 1 - screensplit)
    {
        updateRendererSizes();
    }

    objects.normal.renderer.render(objects.normal.scene, objects.normal.camera);
    objects.logzbuf.renderer.render(objects.logzbuf.scene, objects.logzbuf.camera);

    stats.update();
}

function onWindowResize()
{
    updateRendererSizes();
}

function onBorderPointerDown()
{
    // activate draggable window resizing bar
    window.addEventListener('pointermove', onBorderPointerMove);
    window.addEventListener('pointerup', onBorderPointerUp);
}

function onBorderPointerMove(ev: PointerEvent)
{
    screensplit = Math.max(0, Math.min(1, ev.clientX / window.innerWidth));
}

function onBorderPointerUp()
{
    window.removeEventListener('pointermove', onBorderPointerMove);
    window.removeEventListener('pointerup', onBorderPointerUp);
}

function onMouseMove(ev: MouseEvent)
{
    mouse[0] = ev.clientX / window.innerWidth;
    mouse[1] = ev.clientY / window.innerHeight;
}

function onMouseWheel(ev: WheelEvent)
{
    const amount = ev.deltaY;
    if (amount === 0) return;
    const dir = amount / Math.abs(amount);
    zoomspeed = dir / 10;

    // Slow down default zoom speed after user starts zooming, to give them more control
    minzoomspeed = 0.001;
}
